package se.webbzon.boltzmann.login;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

public class HTTPPostRequest extends HTTPRequest {
	
	// The message used when locking the request for single data posting
	private static final String singleLock = "A content has already been added to this request!";
	
	// The maximal buffer size
	private static final int maxBufferSize = 1*1024*1024;
	
	// Whatever any content has been added to the request
	private String lock;
	
	/*============================================================
	Constructors
	============================================================*/
	
	/** Creates a new HTTP post request. **/
	public HTTPPostRequest(URL url) throws IOException {
		this(url,null,null);
	}
	
	/** Creates a new HTTP post request. **/
	public HTTPPostRequest(URL url, HTTPCookie[] cookies) throws IOException {
		this(url,null,cookies);
	}
	
	/** Creates a new HTTP post request. **/
	public HTTPPostRequest(URL url, LoginAccess access) throws IOException {
		this(url,access,null);
	}

	/** Creates a new HTTP post request. **/
	public HTTPPostRequest(URL url, LoginAccess access, HTTPCookie[] cookies) throws IOException {
		super(url, access, cookies);
		lock = null;
		HttpURLConnection conn = this.getHttpURLConnection();
		conn.setDoOutput(true);
		conn.setRequestMethod("POST");
	}
	
	/*============================================================
	Public Methods
	============================================================*/
	
	/** Adds a plain text to the HTTP post request. **/
	public void addString(String str) throws IOException {
		checkLock();
		byte[] postBytes = str.getBytes(Charset.forName("UTF-8"));
		HttpURLConnection conn = this.getHttpURLConnection();
		conn.setRequestProperty("Content-Type", "text/plain"); 
		conn.setRequestProperty("Charset", "utf-8");
		conn.setRequestProperty("Content-Length", "" + postBytes.length);
		try (OutputStream writer = conn.getOutputStream()) {
			writer.write(postBytes);
			writer.flush();
		} // AutoCloseable
		lock(singleLock);
	}
	
	/** Adds a binary content to the HTTP post request. A MIME content type must
	 * be provided when calling this method. **/
	public void addBytes(String contentType, byte[] b, int off, int len) throws IOException {
		checkLock();
		HttpURLConnection conn = this.getHttpURLConnection();
		conn.setRequestProperty("Content-Type", contentType); 
		conn.setRequestProperty("Content-Transfer-Encoding", "binary");
		conn.setRequestProperty("Content-Length", "" + len);
		try (OutputStream writer = conn.getOutputStream()) {
			writer.write(b, off, len);
			writer.flush();
		} // AutoCloseable
		lock(singleLock);
	}
	
	/** Adds a binary content to the HTTP post request from the given input stream. 
	 * The data length and a MIME content type must be provided when calling this method. **/
	public void addInputStream(String contentType, InputStream is, int len) throws IOException {
		checkLock();
		HttpURLConnection conn = this.getHttpURLConnection();
		conn.setRequestProperty("Content-Type", contentType); 
		conn.setRequestProperty("Content-Transfer-Encoding", "binary");
		conn.setRequestProperty("Content-Length", "" + len);
		
		int bufferSize = Math.min(maxBufferSize, is.available());
		byte[] buffer = new byte[bufferSize];
		int available = Math.min(len, bufferSize);
		try (OutputStream writer = conn.getOutputStream()) {
			while (available > 0) {
				is.read(buffer, 0, available);
				writer.write(buffer, 0, available);
				len -= available;
				available = Math.min(len, Math.min(maxBufferSize, is.available()));
			}
			writer.flush();
		} // AutoCloseable
		lock(singleLock);
	}
	
	/** Adds a single URL-encoded form field to the HTTP post request. 
	 * The provided key and value will be URL-encoded before they are
	 * added to the request. **/
	public void addField(String key, String value) throws IOException {
		Map<String,String> map = new HashMap<String,String>();
		map.put(key, value);
		addFields(map);
	}
	
	/** Adds a map of URL-encoded form fields to the HTTP post request. 
	 * The provided key and value of each field will be URL-encoded before 
	 * they are added to the request. **/
	public void addFields(Map<String,String> fields) throws IOException {
		checkLock();
		StringBuilder builder = new StringBuilder();
		String key = null, value = null;
		boolean first = true;
		
		for (Entry<String,String> field : fields.entrySet()) try {
			key = URLEncoder.encode(field.getKey(),"UTF-8");
			value = field.getValue() == null ? "" : "=" + URLEncoder.encode(field.getValue(),"UTF-8");
			if (first) {
				first = false;
				builder.append(key + value);
			} else
				builder.append("&" + key + value);
		} catch (UnsupportedEncodingException e) {
			throw new IllegalArgumentException("An error occurred when encoding the field: " + key + "=" + value);
		}
		
		byte[] postBytes = builder.toString().getBytes(Charset.forName("UTF-8"));
		HttpURLConnection conn = this.getHttpURLConnection();
		conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); 
		conn.setRequestProperty("Charset", "utf-8");
		conn.setRequestProperty("Content-Length", "" + postBytes.length);
		try (OutputStream writer = conn.getOutputStream()) {
			writer.write(postBytes);
			writer.flush();
		} // AutoCloseable
		lock(singleLock);
	}
	
	/*============================================================
	Protected Methods
	============================================================*/
	
	/** Locks the post request with the provided message. **/
	protected void lock(String message) {
		lock = message;
	}
	
	/** Unlocks the post request. **/
	protected void unlock() {
		lock = null;
	}
	
	/** Returns true if the post request is locked. **/
	protected boolean isLocked() {
		return (lock != null);
	}
	
	/** Returns the lock message of this post request. **/
	protected String getLock() {
		return lock;
	}
	
	/** Throws an IOException is the request is locked. **/
	protected void checkLock() throws IOException {
		if (lock != null) throw new IOException(lock);
	}

}
